﻿// Copyright (c) Appeon
// The following has been modified by Appeon under compliance with the Apache 2.0 license from the original work

// Copyright(c) 2002-2019, Npgsql
// Permission to use, copy, modify, and distribute this software and its
// documentation for any purpose, without fee, and without a written agreement
// is hereby granted, provided that the above copyright notice and this
// paragraph and the following two paragraphs appear in all copies.
//
// IN NO EVENT SHALL NPGSQL BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
// SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS,
// ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
// Npgsql HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// NPGSQL SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED
// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
// PURPOSE.THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND Npgsql
// HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS,
// OR MODIFICATIONS.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using JetBrains.Annotations;
using Npgsql;
using NpgsqlTypes;

namespace SnapObjects.Data.PostgreSql
{
    public class PostgreSqlArrayListTypeMapping : PostgreSqlTypeMapping
    {
        public PostgreSqlArrayListTypeMapping([NotNull] string dbTypeName, [NotNull] TypeMapping elementMapping)
            : base(dbTypeName,
                  typeof(List<>).MakeGenericType(elementMapping.ClrType),
                  GetNpgsqlDbType(elementMapping))
        {
            this.ElementMapping = elementMapping;
        }

        public TypeMapping ElementMapping { get; }

        public override bool AssignCompatiable(Type assignedType)
        {
            if (assignedType.IsArray)
            {
                var dbType = TypeHelper.UnwrapEnumType(TypeHelper.UnwrapNullableType(this.ElementMapping.ClrType));
                var modelType = TypeHelper.UnwrapEnumType(TypeHelper.UnwrapNullableType(assignedType));

                var modelElementType = modelType.GetElementType();

                return dbType == modelElementType;
            }

            return base.AssignCompatiable(assignedType);
        }

        public override object ChangeDbType(object dbValue)
        {
            if (Convert.IsDBNull(dbValue) || dbValue == null)
            {
                return null;
            }

            var valueType = dbValue.GetType();

            if (valueType.IsArray)
            {
                if (this.ElementMapping.ClrType != null &&
                    this.ElementMapping.ClrType == typeof(BitArray) &&
                    dbValue is bool[] values)
                {
                    var bits = new BitArray[values.Length];

                    for (var i = 0; i < values.Length; i++)
                    {
                        bits[i] = new BitArray(new[] { values[i] });
                    }

                    return bits;
                }

                return dbValue;
            }
            else
            {
                return Convert.ChangeType(dbValue, this.ClrType);
            }
        }

        protected override string GenerateNonNullSqlLiteral(object value)
        {
            var type = value.GetType();

            if (!type.IsArray && !(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)))
            {
                throw new ArgumentException("Parameter must be an array or List<>", nameof(value));
            }

            if (value is Array array && array.Rank != 1)
            {
                throw new NotSupportedException("Multidimensional array literals aren't supported");
            }

            var list = (IList)value;

            var sb = new StringBuilder();

            sb.Append("ARRAY[");

            for (var i = 0; i < list.Count; i++)
            {
                sb.Append(this.ElementMapping.GenerateSqlLiteral(list[i]));

                if (i < list.Count - 1)
                {
                    sb.Append(",");
                }
            }

            sb.Append("]::");

            sb.Append(this.ElementMapping.DbTypeName);

            sb.Append("[]");

            return sb.ToString();
        }

        private static NpgsqlDbType GetNpgsqlDbType(TypeMapping elementMapping)
        {
            var elementDbType = elementMapping is PostgreSqlTypeMapping elementNpgsqlTypeMapping
                ? elementNpgsqlTypeMapping.NpgsqlDbType
                : elementMapping.DbType.HasValue
                ? new NpgsqlParameter { DbType = elementMapping.DbType.Value }.NpgsqlDbType
                : default(NpgsqlDbType?);

            return elementDbType == null
                ? NpgsqlDbType.Array : NpgsqlDbType.Array | elementDbType.Value;
        }

        public override TypeMapping Clone([NotNull] string dbTypeName, int? size)
        {
            return new PostgreSqlArrayListTypeMapping(this.DbTypeName, this.ElementMapping);
        }
    }
}
